Leaflet Blog in Deno Fresh
1/** @jsxImportSource preact */ 2import { CSS, render } from "@deno/gfm"; 3import { Handlers, PageProps } from "$fresh/server.ts"; 4 5import { Footer } from "../../components/footer.tsx"; 6import { PostInfo } from "../../components/post-info.tsx"; 7import { Title } from "../../components/typography.tsx"; 8import { getPost } from "../../lib/api.ts"; 9import { Head } from "$fresh/runtime.ts"; 10 11interface Post { 12 uri: string; 13 value: { 14 title: string; 15 content: string; 16 createdAt: string; 17 }; 18} 19 20// Only override backgrounds in dark mode to make them transparent 21const transparentDarkModeCSS = ` 22@media (prefers-color-scheme: dark) { 23 .markdown-body { 24 color: white; 25 background-color: transparent; 26 } 27 28 .markdown-body a { 29 color: #58a6ff; 30 } 31 32 .markdown-body blockquote { 33 border-left-color: #30363d; 34 background-color: transparent; 35 } 36 37 .markdown-body pre, 38 .markdown-body code { 39 background-color: transparent; 40 color: #c9d1d9; 41 } 42 43 .markdown-body table td, 44 .markdown-body table th { 45 border-color: #30363d; 46 background-color: transparent; 47 } 48} 49 50.font-sans { font-family: var(--font-sans); } 51.font-serif { font-family: var(--font-serif); } 52.font-mono { font-family: var(--font-mono); } 53 54.markdown-body h1 { 55 font-family: var(--font-serif); 56 text-transform: uppercase; 57 font-size: 2.25rem; 58} 59 60.markdown-body h2 { 61 font-family: var(--font-serif); 62 text-transform: uppercase; 63 font-size: 1.75rem; 64} 65 66.markdown-body h3 { 67 font-family: var(--font-serif); 68 text-transform: uppercase; 69 font-size: 1.5rem; 70} 71 72.markdown-body h4 { 73 font-family: var(--font-serif); 74 text-transform: uppercase; 75 font-size: 1.25rem; 76} 77 78.markdown-body h5 { 79 font-family: var(--font-serif); 80 text-transform: uppercase; 81 font-size: 1rem; 82} 83 84.markdown-body h6 { 85 font-family: var(--font-serif); 86 text-transform: uppercase; 87 font-size: 0.875rem; 88} 89`; 90 91export const handler: Handlers<Post> = { 92 async GET(_req, ctx) { 93 try { 94 const { slug } = ctx.params; 95 const post = await getPost(slug); 96 return ctx.render(post); 97 } catch (error) { 98 console.error("Error fetching post:", error); 99 return new Response("Post not found", { status: 404 }); 100 } 101 }, 102}; 103 104export default function BlogPage({ data: post }: PageProps<Post>) { 105 if (!post) { 106 return <div>Post not found</div>; 107 } 108 109 return ( 110 <> 111 <Head> 112 <title>{post.value.title} knotbin</title> 113 <meta name="description" content="by Roscoe Rubin-Rottenberg" /> 114 {/* Merge GFM’s default styles with our dark-mode overrides */} 115 <style 116 dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }} 117 /> 118 </Head> 119 120 <div className="grid grid-rows-[20px_1fr_20px] justify-items-center min-h-dvh py-8 px-4 xs:px-8 pb-20 gap-16 sm:p-20"> 121 <link rel="alternate" href={post.uri} /> 122 <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px] overflow-hidden"> 123 <article className="w-full space-y-8"> 124 <div className="space-y-4 w-full"> 125 <a 126 href="/" 127 className="hover:underline hover:underline-offset-4 font-medium" 128 > 129 Back 130 </a> 131 <Title>{post.value.title}</Title> 132 <PostInfo 133 content={post.value.content} 134 createdAt={post.value.createdAt} 135 includeAuthor 136 className="text-sm" 137 /> 138 <div className="diagonal-pattern w-full h-3" /> 139 </div> 140 <div className="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0"> 141 {/* Render GFM HTML via dangerouslySetInnerHTML */} 142 <div 143 class="mt-8 markdown-body" 144 dangerouslySetInnerHTML={{ __html: render(post.value.content) }} 145 /> 146 </div> 147 </article> 148 </main> 149 <Footer /> 150 </div> 151 </> 152 ); 153}